/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

****/
package org.activequant.data.util.converter;

import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.TimeZone;

import org.activequant.calendar.IExchangeCalendar;
import org.activequant.core.domainmodel.InstrumentSpecification;
import org.activequant.core.domainmodel.data.Candle;
import org.activequant.core.domainmodel.data.TradeIndication;
import org.activequant.core.types.TimeFrame;
import org.activequant.core.types.TimeStamp;
import org.apache.log4j.Logger;

/**
 * Generates a list of TradeIndication objects from a candle 
 * by faking the price move from Open to High/Low, and Close. The generation
 * of trade indication is controlled by the following factors:
 * <ul>
 *   <li> parameter {@link #getNumSteps() numSteps} that determines how many trades are generated per segment. 
 *   <li> whether highTimeStamp and lowTimeStamp are set on the incoming candle. If set, then
 *        these timestamps are used to generate segments. If not set, we proceed as if
 *        high timestamp was set at the 1/3 of the candle timeframe span, 
 *        and low timestamp at 2/3.
 *   <li> if input is an EOD candle (@{link TimeFrame.day()}), and {@link #exchangeCalendar}
 *        property is set, then opening timestamp is determined by asking the exchangeCalendar.
 *        Else, opening timestamp is inferred from candles's timeFrame, and candle's 
 *        timestamp (which is closing timestamp). After this, if the computed high timestamp
 *        is less than the low timestamp, we generate three linearly interpolated segments:
 *        from Open to High, to Low, and to Close. Else, we generate three linearly interpolated
 *        segments from Open to Low, to High, and to Close.
 * </ul> 
 * <p>
 * The default value for <code>numSteps</code> is 1, which makes the code to generate
 * four trade indication events per candle: at Open, at High/Low, and at Close.
 * <p>
 * The date, and price are linearly interpolated within a segment. Candle's
 * volume is evenly spread across all generated tradeIndications.
 * <br/>
 * Implements the ICandleToTradeIndicationConversion interface. Holds the following associated variables:
 * <ul>
 * <li>numSteps(int)</li>
 * <li>exchangeCalendar(IExchangeCalendar)</li>
 * </ul>
 * <b>History:</b><br>
 *  - [10.07.2007] Created (Ulrich Staudinger)<br>
 *  - [05.08.2007] Cleanup (Erik Nijkamp)<br>
 *  - [29.09.2007] cleanup + moved to new domain model (Erik Nijkamp)<br>
 *  - [29.09.2007] Refactored push/pull approach (Erik Nijkamp)<br>
 *  - [09.11.2007] Moved to subscription data model (Mike Kroutikov)<br>
 *  - [09.30.2008] Isolated generation code following a strategy pattern (Mike Kroutikov)<br>
 *  - [10.04.2008] Honor candle's high/low timestamps, use calendar for EOD candles (Mike K from code by  Mark B)
 *
 *  @author Ulrich Staudinger
 */
public class LinearCandleToTradeIndicationConversion implements ICandleToTradeIndicationConversion {
	private static Logger log = Logger.getLogger(LinearCandleToTradeIndicationConversion.class);
	/**
	 * private int numSteps = 1;<br/>
	 * default is one trade per step
	 */
	private int numSteps = 1;
	/**
	 * private IExchangeCalendar exchangeCalendar = null;<br/>
	 * optional
	 */
	private IExchangeCalendar exchangeCalendar = null;

	/**
	 * Determines how many trades to generate per segment.
	 * Default value is 1, which means that only one trade per segment is
	 * generated.<br/>
	 * returns the associated numSteps(int)
	 * @return number of steps;
	 */
	public int getNumSteps() {
		return numSteps;
	}

	/**
	 * Sets number of steps.<br/>
	 * sets the associated numSteps(int) with the given val(int) (can't be less than 1)
	 * @param val number of steps.
	 */
	public void setNumSteps(int val) {
		if(val < 1) {
			throw new IllegalArgumentException("numSteps must be positive");
		}
		numSteps = val;
	}
	
	/**
	 * Optional property: if set, will be used for EOD candles to determine
	 * the start of time stamps assigned to generated TradeIndications.<br/>
	 * returns the associated exchangeCalendar(IExchangeCalendar)
	 * @return exchange calendar.
	 */
	public IExchangeCalendar getExchangeCalendar() {
		return exchangeCalendar;
	}
	
	/**
	 * Sets the exchange calendar.<br/>
	 * sets the associated exchangeCalendar(IExchangeCalendar) with the given val(IExchangeCalendar)
	 * @param val exchange calendar.
	 */
	public void setExchangeCalendar(IExchangeCalendar val) {
		exchangeCalendar = val;
	}
	/**
	 * Adds a total of <strong>numSteps(int)</strong> TradeIndications to the <strong>given collector(List&lt;TradeIndication&gt;)</strong><br/>
	 * Each TradeIndication has its associated variables set as following:<br/>
	 * -instrumentSpecification(InstrumentSpecification) set with the <strong>given spec(InstrumentSpecification)</strong><br/>
	 * -timeStamp(TimeStamp) ranging incrementally from the <strong>given nanosStart(long) to nanosEnd(long)</strong><br/>
	 * -price(double) ranging incrementally from the <strong>given priceStart(double) to priceEnd(double)</strong><br/>
	 * -quantity(double) set with the <strong>given quantity(double)</strong>
	 * @param numSteps
	 * @param priceStart
	 * @param priceEnd
	 * @param nanosStart
	 * @param nanosEnd
	 * @param quantity
	 * @param spec
	 * @param collector
	 */
	private static void buildSegment(int numSteps, double priceStart, double priceEnd, long nanosStart, long nanosEnd, double quantity, InstrumentSpecification spec, List<TradeIndication> collector) {
		double priceStep = (priceEnd - priceStart) / numSteps;
		long timeStep = (nanosEnd - nanosStart) / numSteps;
		for(int i  = 0; i < numSteps; i++) {
    		collector.add(new TradeIndication(spec, new TimeStamp(nanosStart), priceStart, quantity));
    		priceStart += priceStep;
    		nanosStart += timeStep;
		}
	}

	/**
	 * Generates numStaps events, reaching priceEnd, and nanoeEnd.<br/>
	 * Adds a total of <strong>numSteps(int)+1</strong> TradeIndications to the <strong>given collector(List&lt;TradeIndication&gt;)</strong><br/>
	 * Each TradeIndication has its associated variables set as following:<br/>
	 * -instrumentSpecification(InstrumentSpecification) set with the <strong>given spec(InstrumentSpecification)</strong><br/>
	 * -timeStamp(TimeStamp) ranging incrementally from the <strong>given nanosStart(long) to nanosEnd(long)+timeStep</strong><br/>
	 * -price(double) ranging incrementally from the <strong>given priceStart(double) to priceEnd(double)+priceStep</strong><br/>
	 * -quantity(double) set with the <strong>given quantity(double)</strong>
	 */
	private static void buildLastSegment(int numSteps, double priceStart, double priceEnd, long nanosStart, long nanosEnd, double quantity, InstrumentSpecification spec, List<TradeIndication> collector) {
		double priceStep = (priceEnd - priceStart) / numSteps;
		long timeStep = (nanosEnd - nanosStart) / (numSteps);
		for(int i  = 0; i <= numSteps; i++) {
    		collector.add(new TradeIndication(spec, new TimeStamp(nanosStart), priceStart, quantity));
    		priceStart += priceStep;
    		nanosStart += timeStep;
		}
	}
	/**
	 * returns a List&lt;TradeIndication&gt; by converting the given candle(Candle) into a total of (3*numSteps(int)+1) TradeIndication where:<br/>
	 * -numSteps(int) TradeIndications are from start-low or start-high<br/>
	 * -numSteps(int) TradeIndications are from low-high or high-low<br/>
	 * -numSteps(int)+1 TradeIndications are from high-finish or low-finish<br/>
	 * Rounds up the open-close times in order to get start-finish times with the appropriate number of steps in between.
	 */
	public List<TradeIndication> convert(Candle candle) {
		List<TradeIndication> out = new LinkedList<TradeIndication>();		
		TimeFrame frame = candle.getTimeFrame();
		if(frame == null) {
			throw new IllegalArgumentException("candle duration must be known");
		}

		long timestampClose;
		long timestampOpen;
		if(frame.equals(TimeFrame.day()) && exchangeCalendar != null) {
			timestampOpen = exchangeCalendar.getOpeningTimeTodayAsTimeStamp(candle.getTimeStamp()).getNanoseconds();
			timestampClose= exchangeCalendar.getCloseTimeTodayAsTimeStamp(candle.getTimeStamp()).getNanoseconds();
		} else {
			Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
			calendar.setTime(candle.getTimeStamp().getDate()); // candle's date is CLOSE date!
			calendar.add(Calendar.MILLISECOND, -1); // to make sure that align does move date backwards
			
			frame.alignCalendar(calendar);
			timestampOpen = new TimeStamp(calendar.getTime()).getNanoseconds();
			timestampClose = candle.getTimeStamp().getNanoseconds();
		}
		
		long timestampStart;
		long timestampFinish;
		long timestampHigh;
		long timestampLow;
		if(candle.getHighTimeStamp() != null && candle.getLowTimeStamp() != null) {
			timestampHigh = candle.getHighTimeStamp().getNanoseconds();
			timestampLow  = candle.getLowTimeStamp().getNanoseconds();
			timestampStart = timestampOpen + (timestampHigh - timestampOpen) / (numSteps + 1);
			if(timestampHigh < timestampLow) {
				timestampFinish = timestampLow + (timestampClose - timestampLow) / (numSteps + 2); 
			} else {
				timestampFinish = timestampHigh + (timestampClose - timestampHigh) / (numSteps + 2); 
			}
		} else {
			long interval = (timestampClose - timestampOpen) / (6 * numSteps + 2);
			timestampStart = timestampOpen + interval;
			timestampHigh = timestampOpen + (2 * numSteps + 1) * interval;
			timestampLow = timestampOpen + (4 * numSteps + 1) * interval;
			timestampFinish = timestampOpen + (6 * numSteps + 1) * interval;
		}
		
		// some sanity checks
		if(timestampFinish < timestampStart) {
			// bad candle: what should we do?
			log.warn("close timestamp is less than open timestamp (fixed): " + candle);
			timestampStart = timestampFinish;
		}
		if(timestampHigh < timestampStart) {
			// bad candle: what should we do?
			log.warn("high timestamp is less than open timestamp (fixed): " + candle);
			timestampHigh = timestampStart;
		}
		if(timestampHigh > timestampFinish) {
			// bad candle: what should we do?
			log.warn("high timestamp is greater than close timestamp (fixed): " + candle);
			timestampHigh = timestampFinish;
		}
		if(timestampLow < timestampStart) {
			// bad candle: what should we do?
			log.warn("low timestamp is less than open timestamp (fixed): " + candle);
			timestampLow = timestampStart;
		}
		if(timestampLow > timestampFinish) {
			// bad candle: what should we do?
			log.warn("low timestamp is greater than close timestamp (fixed): " + candle);
			timestampLow = timestampFinish;
		}

		InstrumentSpecification spec = candle.getInstrumentSpecification();
		double quantity = candle.getVolume() / (numSteps * 3 + 1);
		
		if(timestampHigh <= timestampLow) {
			buildSegment(numSteps, 
					candle.getOpenPrice(), 
					candle.getHighPrice(), 
					timestampStart,
					timestampHigh,
					quantity,
					spec,
					out);
			buildSegment(numSteps, 
					candle.getHighPrice(), 
					candle.getLowPrice(), 
					timestampHigh,
					timestampLow,
					quantity,
					spec,
					out);
			buildLastSegment(numSteps, 
					candle.getLowPrice(), 
					candle.getClosePrice(), 
					timestampLow,
					timestampFinish,
					quantity,
					spec,
					out);
		} else { // low-to-high
			buildSegment(numSteps, 
					candle.getOpenPrice(), 
					candle.getLowPrice(), 
					timestampStart,
					timestampLow,
					quantity,
					spec,
					out);
			buildSegment(numSteps, 
					candle.getLowPrice(), 
					candle.getHighPrice(), 
					timestampLow,
					timestampHigh,
					quantity,
					spec,
					out);
			buildLastSegment(numSteps, 
					candle.getHighPrice(), 
					candle.getClosePrice(), 
					timestampHigh,
					timestampFinish,
					quantity,
					spec,
					out);
		}

		if(log.isDebugEnabled()) {
			log.debug("one candle " + candle + " generated " + out.size() + " trade indications");
		}

		return out;
	}
}